package com.google.code.jouka.utils;


import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * <PRE>
 * A mutable sequence of characters.
 * byte値の可変シーケンス。
 *
 * Example :
 *
 *   // =================================================
 *   // ==== basic usage
 *   // =================================================
 *   byte[] data = {0x20, 0x31, 0x32, 0x33, 0x41, 0x42, 0x43};
 *   ByteBuilder bb = new ByteBuilder();
 *   bb.append(data);
 *   String str = bb.newString();
 *   // str = " 123ABC"
 *
 *
 *   // =================================================
 *   // ==== * append byte data
 *   // ==== * length & capacity expansion of ByteBuffer
 *   // =================================================
 *   // create new ByteBuilder with capacity = 4 bytes
 *   ByteBuilder bb = new ByteBuilder(4);
 *   // --> bb.length()   = 0
 *   // --> bb.capacity() = 4
 *
 *   // append bytes to ByteBuilder
 *   bb.append(0x01).append(0x02).append(0x03).append(0x04);
 *   // --> bb.length()   = 4
 *   // --> bb.capacity() = 4
 *
 *   // 1st auto expansion of capacity
 *   bb.append(0x05);
 *   // --> bb.length()   = 5
 *   // --> bb.capacity() = 6
 *
 *   // fill to limit of capacity
 *   bb.append(0x06);
 *   // --> bb.length()   = 6
 *   // --> bb.capacity() = 6
 *
 *   // 2nd auto expansion of capacity
 *   bb.append(0x06);
 *   // --> bb.length()   = 7
 *   // --> bb.capacity() = 9
 *
 *   bb.append(0x7F);
 *   // --> bb.length()   = 8
 *   // --> bb.capacity() = 9
 *
 *   byte[] data = bb.toByteArray();
 *   // --> data = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x7F}
 *
 *
 * </PRE>
 * @author Str.Isym
 *
 */
public class ByteBuilder {
    private int _position = 0;
    private byte[] _buffer = null;
    private String _formatParam= "0x%02x";
    private double _BuffExpandMultipler = 1.5;
    private Object _referObject = null;

    /**
     * <PRE>
     * インスタンスをデフォルトの容量=1024バイトで作成する。
     * この容量は、ByteBuilder{@link #capacity()}で取得できる値。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder();
     *
     *     // bb.capacity() = 1024 bytes
     *     // bb.length()   = 0
     *
     * </PRE>
     */
    public ByteBuilder() {
        this(1024);
    }

    /**
     * <PRE>
     * インスタンスを指定の容量(バイト数)で作成する。
     * この容量は、{@link #capacity()}で取得できる値。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(32);
     *
     *     // bb.capacity() = 32 bytes
     *     // bb.length()   = 0
     *
     * </PRE>
     *
     * @param capacity 格納できるbyte値の初期容量
     */
    public ByteBuilder(int capacity) {
        _buffer = new byte[capacity];
    }

    /**
     * <PRE>
     * 初期値として与えられたbyte配列を内包したインスタンスを作成する。
     * 初期値となるbyte配列がnullの場合、サイズがゼロのインスタンスとなる。
     *
     * Example :
     *     byte[] initValues = {0x41, 0x42, 0x43};
     *     ByteBuilder bb = new ByteBuilder(initValues);
     *
     *     // bb.capacity() = 3
     *     // bb.length()   = 3
     *
     *     bb.append(0x24).append(0x25);
     *
     *     // bb.capacity() = 6 <- auto expanded capacity
     *     // bb.length()   = 5
     *
     * </PRE>
     *
     * @param initValues 初期値として与えるbyte配列
     */
    public ByteBuilder(byte[] initValues) {
        if(initValues == null){
            initValues = new byte[0];
        }
        _buffer = initValues;
        _position = initValues.length;
    }


    /**
     * <PRE>
     * 初期値として与えられたByteBufferの内容をbyte配列化して、
     * それを内包したインスタンスを作成する。
     * byte配列化される際の配列長は、ByteBufferのcapacityに依存する。
     *
     *
     * </PRE>
     *
     * @param initBuff 初期値として与えるByteBuffer
     */
    public ByteBuilder(ByteBuffer initBuff) {
        if(initBuff == null){
            throw new IllegalArgumentException("initBuff is NULL.");
        }
        if(initBuff.capacity() == 0){
            throw new IllegalArgumentException("initBuff.capacity() = 0");
        }
        _buffer = new byte[initBuff.limit()];
        initBuff.position(0);
        initBuff.get(_buffer);
        _position = initBuff.limit();
        initBuff.position(0);
    }


    /**
     * <PRE>
     * ByteBuiler末尾にint値の下位8bitのbyte値を追加する。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(1);
     *     int value = 0x89AB;
     *     bb.append(value);
     *
     *     //  bb.get(0) = (byte)0xAB
     *
     * </PRE>
     *
     * @param value 追加したいbyte値を下位8bitに含むint値
     * @return byte値の追加が完了したByteBuiler
     */
    public ByteBuilder append(int value) {
        append((byte)(value & 0xFF));
        return this;
    }

    /**
     * <PRE>
     * ByteBuiler末尾にbyte値を追加する。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(3);
     *     bb.append((byte)0x12).append((byte)0x34).append((byte)0x56);
     *
     *     // bb.get(0) = (byte)0x12
     *     // bb.get(1) = (byte)0x34
     *     // bb.get(2) = (byte)0x56
     *     // bb.get(3) = java.lang.ArrayIndexOutOfBoundsException
     *
     * </PRE>
     *
     * @param value 追加したいbyte値
     * @return byte値の追加が完了したByteBuiler
     */
    public ByteBuilder append(byte value) {
        if (_position + 1 > _buffer.length) {
            byte[] newBuffer = expandCapacity();
            _buffer = newBuffer;
        }
        _buffer[_position++] = (byte)value;
        return this;
    }


    /**
     * <PRE>
     * ByteBuiler末尾に、byte配列を追加する。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(3);
     *     bb.append((byte)0x12);
     *     byte[] newValues = {(byte)0xAB, (byte)0xCD};
     *     bb.append(newValues);
     *
     *     // bb.get(0) = (byte)0x12
     *     // bb.get(1) = (byte)0xAB
     *     // bb.get(2) = (byte)0xCD
     *
     * </PRE>
     *
     * @param values 追加したいbyte配列データ
     * @return byte値の追加が完了したByteBuiler
     */
    public ByteBuilder append(byte[] values) {
        if(values == null){
            throw new IllegalArgumentException("values is NULL.");
        }
        if(values.length == 0){
            throw new IllegalArgumentException("values.length = 0");
        }
        append(values, 0, values.length);
        return this;
    }


    /**
     * <PRE>
     * ByteBuiler末尾に、byte配列の任意のoffset位置から、
     * 指定のバイト数(len)分を追加する。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(4);
     *     bb.append((byte)0x12);
     *     byte[] newValues = {(byte)0xAB, (byte)0xCD, (byte)0xEF, (byte)0x99, (byte)0x88};
     *     bb.append(newValues, 1, 3);
     *
     *     // bb.get(0) = (byte)0x12
     *     // bb.get(1) = (byte)0xCD
     *     // bb.get(2) = (byte)0xEF
     *     // bb.get(3) = (byte)0x99
     *
     * </PRE>
     *
     * @param values 追加したい連続したbyte値を含むbyte配列
     * @param offset valuesから追加したいデータの先頭
     * @param len 追加したいデータのバイト数
     * @return byte値の追加が完了したByteBuiler
     */
    public ByteBuilder append(byte[] values, int offset, int len) {
        if(offset + len <= values.length){
            if ((_position + len) > _buffer.length) {
                byte[] newArray = expandCapacity(values);
                _buffer = newArray;
            }
            System.arraycopy(values, offset, _buffer, _position, len);
            _position += len;
            return this;
        }
        throw new IndexOutOfBoundsException(
                "\n    values.length = " + values.length + "\n"
                + "    offset + len  = " + (offset + len) + "\n"
                + "    (offset = " + offset + ", len = " + len + ")\n");
    }


    // 任意の位置のbyte値を削除する
    public ByteBuilder remove(int index){
        if(index == 0){
            return removeFirst();
        }
        if(index == _buffer.length - 1){
            return removeLast();
        }
        if(validIndex(index)){
            byte[] newArray = new byte[_buffer.length - 1];
            System.arraycopy(_buffer, 0        , newArray, 0     , index);
            System.arraycopy(_buffer, index + 1, newArray, index , newArray.length - index);
            _buffer = newArray;
            _position--;
        }
        return this;
    }

    public ByteBuilder removeFirst(){
        if(validIndex(0)){
            byte[] newArray = new byte[_buffer.length - 1];
            System.arraycopy(_buffer, 1, newArray, 0, _buffer.length - 1);
            _buffer = newArray;
            _position--;
        }
        return this;
    }

    public ByteBuilder removeLast(){
        if(validIndex(_buffer.length - 1)){
            byte[] newArray = new byte[_buffer.length - 1];
            System.arraycopy(_buffer, 0, newArray, 0, _buffer.length - 1);
            _buffer = newArray;
            _position--;
        }
        return this;
    }

    private boolean validIndex(int index){
        if(index < 0 || index >=_buffer.length){
            throw new IndexOutOfBoundsException(
                    "\n    Invalid index:"
                  + "\n    buff.length = " + _buffer.length
                  + "\n    index       = " + index
            );
        }
        if(_buffer.length == 0){
            throw new IndexOutOfBoundsException(
                    "\n    Invalid index or buff-size:"
                  + "\n    buff.length = " + _buffer.length
                  + "\n    index       = " + index
            );
        }
        return true;
    }



    public ByteBuilder append(ByteBuffer buff, int offset, int len) {
        if(buff == null){
            throw new IllegalArgumentException("buff is NULL.");
        }
        if(offset + len <= buff.limit()){
            byte[] newArray = new byte[len];
            for(int i=0; i<len; i++){
                newArray[i] = buff.get(offset + i);
            }
            this.append(newArray);
            return this;
        }
        throw new IndexOutOfBoundsException(
                "\n    buff.limit   = " + buff.limit() + "\n"
                + "    offset + len = " + (offset + len) + "\n"
                + "    (offset = " + offset + ", len = " + len + ")\n");
    }

    public ByteBuilder append(ByteBuffer buff) {
        if(buff == null){
            throw new IllegalArgumentException("buff is NULL.");
        }
        byte[] newArray = new byte[buff.limit()];
        buff.position(0);
        //buff.get(newArray);
        for(int i=0; i<newArray.length; i++){
            newArray[i] = buff.get(i);
        }

        this.append(newArray);
        return this;
    }


    /**
     * <PRE>
     * バッファ容量を拡張する
     * </PRE>
     *
     * @return
     */
    private byte[] expandCapacity(){
        int newCapacity = (int)((_buffer.length) * _BuffExpandMultipler);
        byte[] newBuff = Arrays.copyOf(_buffer, newCapacity);
        return newBuff;
    }

    /**
     * <PRE>
     * バッファ容量を拡張する
     * </PRE>
     *
     * @param src
     * @return
     */
    private byte[] expandCapacity(byte[] src){
        int newCapacity = (int)((_buffer.length + src.length) * _BuffExpandMultipler);
        byte[] newBuff = Arrays.copyOf(_buffer, newCapacity);
        return newBuff;
    }

    /**
     * <PRE>
     * ByteBuilderの指定したindex位置にあるbyte値を返す。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder();
     *     byte[] values = {1, 2, 3, 4, (byte)0x7E, (byte)0x7F};
     *     bb.append(values);
     *     byte b1 = bb.get(3); // b1 = (byte)0x04
     *     byte b2 = bb.get(4); // b2 = (byte)0x7E
     *
     * </PRE>
     *
     * @param index 返したいbyte値の位置
     * @return 指定したindex位置にあるbyte値
     */
    public byte get(int index){
        return _buffer[index];
    }


    /**
     * <PRE>
     * ByteBuilderの指定したindex位置に、byte値をセットする。
     *
     * Example :
     *     byte[] values = {(byte)1, (byte)2, (byte)3, (byte)4};
     *     ByteBuilder bb = new ByteBuilder(values);
     *     bb.put(0, (byte)0xFF).put(3, (byte)0x99);
     *	   byte[] result = bb.toByteArray();
     *
     *     // result = {0xFF, 0x02, 0x03, 0x99}
     *
     * </PRE>
     *
     * @param index セット対象の位置
     * @param value セットしたい値
     * @return 自分自身のインスタンス
     */
    public ByteBuilder put(int index, byte value){
        if(index < _buffer.length){
            _buffer[index] = value;
            return this;
        }
        throw new IndexOutOfBoundsException(
                "\n    index         = " + index + "\n"
                + "    buffer.length = " + _buffer.length
                + " (index-range : 0 - " + (_buffer.length - 1) + ")"
        );
    }

    /**
     * <PRE>
     * ByteBuilderの指定したindex位置に、int値の下位8ビットのbyte値をセットする。
     *
     * Example :
     *	   byte[] values = {(byte)6, (byte)7, (byte)8, (byte)9};
     *     ByteBuilder bb = new ByteBuilder(values);
     *     bb.put(0,  0xFF01); // (byte)0x01 =>  1 as int
     *     bb.put(1,  0xFFFF); // (byte)0xFF => -1 as int
     *     byte[] result = bb.toByteArray());
     *
     *     // result = {0x01, 0xFF, 0x08, 0x09}
     *
     *
     * </PRE>
     *
     * @param index
     * @param value
     * @return 自分自身のインスタンス
     */
    public ByteBuilder put(int index, int value){
        return this.put(index, (byte)(value & 0xFF));
    }


    /**
     * <PRE>
     * ByteBuilderの内容を、byte配列に変換して返す。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder();
     *     bb.append((byte)0x7E).append((byte)0x8F);
     *     byte[] result = bb.toByteArray();
     *
     *     // result[0] = 0x7E
     *     // result[1] = 0x8F
     *
     * </PRE>
     *
     * @return 変換されたbyte配列
     */
    public byte[] toByteArray(){
        if(_position < _buffer.length){
            return Arrays.copyOfRange(_buffer, 0, _position);
        }
        return _buffer;
    }


    /**
     * <PRE>
     * ByteBuilderの内容を、offsetで指定される位置からlenで指定されるバイト数分、
     * byte配列に変換して返す。
     *
     * Example :
     * 	   ByteBuffer buff = ByteBuffer.allocate(8);
     *     buff.put((byte)1).put((byte)2).put((byte)3).put((byte)4);
     *
     *     // contents of buff:
     *     // +--+--+--+--+--+--+--+--+
     *     // |01|02|03|04|00|00|00|00|
     *     // +--+--+--+--+--+--+--+--+
     *     //  0  1  2  3  4  5  6  7  <- offset
     *
     *     ByteBuilder bb = new ByteBuilder(buff);
     *     byte[] result = bb.toByteArray(3, 5);
     *     // result = {4, 0, 0, 0, 0}
     *
     *     // IndexOutOfBoundsException
     *     byte[] result = bb.toByteArray(3, 6);
     *
     *     // IndexOutOfBoundsException
     *     result = bb.toByteArray(8, 1);
     *
     * </PRE>
     *
     * @param offset 開始位置
     * @param len 返して欲しいバイト数
     * @return 変換されたbyte配列
     * @throws IndexOutOfBoundsException
     */
    public byte[] toByteArray(int offset, int len) throws IndexOutOfBoundsException{
        if(offset + len <= _buffer.length){
            return Arrays.copyOfRange(_buffer, offset, offset + len);
        }
        throw new IndexOutOfBoundsException(
                "\n    ByteBuilder.length = " + _buffer.length + "\n"
                + "    offset + len       = " + (offset + len) + "\n"
                + "    (offset = " + offset + ", len = " + len + ")\n");
    }


    /**
     * <PRE>
     * ByteBuilderに格納されている、有効なデータ長を返す。
     * ByteBuilder自体の容量を取得したい場合は、{@link #capacity()}を用いる。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(5);
     *     // bb.length() = 0
     *
     *     byte[] values = {1, 2, 3, 4};
     *     bb.append(values);
     *     // bb.length() = 4
     *
     *     bb.append((byte)0xFF);
     *     // bb.length() = 5
     *
     * </PRE>
     *
     * @return ByteBuilderに格納されている有効なデータ長
     */
    public int length() {
        return _position;
    }

    /**
     * <PRE>
     * ByteBuilder自体の総容量を返す。
     * ByteBuilderに格納されている、有効なデータ長を返したい場合は、
     * {@link #length()}を用いる。
     *
     * Example :
     *     ByteBuilder bb = new ByteBuilder(4);
     *     // bb.length()   = 0
     *     // bb.capacity() = 4
     *
     *     byte[] values = {1, 2, 3, 4};
     *     bb.append(values);
     *     // bb.length()   = 4
     *     // bb.capacity() = 4
     *
     *     bb.append((byte)0xFF);
     *     // bb.length()   = 5
     *     // bb.capacity() = 6  <- auto expanded size
     *
     * </PRE>
     *
     * @return ByteBuilder自体の総容量
     */
    public int capacity() {
        return _buffer.length;
    }


    /**
     * <PRE>
     * ByteBuilderに格納されているbyte配列を用いて、新たな文字列を生成して返す。
     *
     * Example :
     *     byte[] initValues = {0x41, 0x42, 0x43}; // <- initValues = "ABC"
     *     ByteBuilder bb = new ByteBuilder(initValues);
     *
     *     // bb.capacity() = 3
     *     // bb.length()= 3
     *
     *     bb.append(0x24); // bb.capacity() =  4, bb.length() = 4  <- auto expanded capacity
     *     bb.append(0x25); // bb.capacity() =  6, bb.length() = 5  <- auto expanded capacity
     *     bb.append(0x26); // bb.capacity() =  6, bb.length() = 6
     *     bb.append(0x27); // bb.capacity() =  9, bb.length() = 7  <- auto expanded capacity
     *     bb.append(0x28); // bb.capacity() =  9, bb.length() = 8
     *     bb.append(0x29); // bb.capacity() =  9, bb.length() = 9
     *     bb.append(0x30); // bb.capacity() = 13, bb.length() = 10 <- auto expanded capacity
     *
     *     String result = bb.newString();
     *
     *     // result = "ABC$%&'()"
     *
     * </PRE>
     *
     * @return 生成された文字列
     */
    public String newString() {
        return new String(_buffer, 0, _position);
    }

    /**
     * <PRE>
     * ByteBuilderに格納されているbyte配列を用い、charsetを指定して
     * 新たな文字列を生成して返す。
     *
     * </PRE>
     *
     * @param charset 文字列を生成(new String())する際に指定する文字エンコード
     * @return 生成された文字列
     * @throws UnsupportedEncodingException
     */
    public String newString(String charset) throws UnsupportedEncodingException {
        return new String(_buffer, 0, _position, charset);
    }

    /**
     * <PRE>
     * ByteBuilderに格納されているbyte配列を用い、charsetを指定して
     * 新たな文字列を生成して返す。
     * その際に、byte配列中でpreAdj、postAdjで指定されるバイト数分を
     * それぞれbyte配列の先頭からと末尾から取り除く。
     *
     *
     * </PRE>
     *
     * @param charset
     * @param preAdj byte配列先頭からスキップするバイト数
     * @param postAdj byte配列末尾から無視するバイト数
     * @return
     * @throws UnsupportedEncodingException
     */
    public String newString(String charset, int preAdj, int postAdj) throws UnsupportedEncodingException {
        return new String(_buffer, preAdj, (_position + postAdj), charset);
    }


    /**
     * <PRE>
     * ByteBuilderに含まれるbyte値を、半角中括弧で囲まれた
     * 16進数文字列の配列形式で返す。
     *
     * Example :
     *     byte[] initValues = {0x41, 0x42, 0x43};
     *     ByteBuilder bb = new ByteBuilder(initValues);
     *     String s = bb.toString();
     *
     *     // s = "{0x41, 0x42, 0x43}"
     *
     * </PRE>
     */
    @Override
    public String toString(){
        if(this.length() == 0){
            return "null";
        }
        StringBuilder sb = new StringBuilder("{");
        for(int i=0; i<_position - 1; i++){
            sb.append(String.format(_formatParam, _buffer[i])).append(", ");
        }
        sb.append(String.format(_formatParam, _buffer[_position - 1])).append("}");
        return sb.toString();
    }

    /**
     * <PRE>
     * インスタンス生成時に確保したバッファ容量を自動拡張する際の倍数を変更する。
     * appendメソッド利用によるバッファ容量溢れが発生した際、デフォルトでは
     * 新しいバッファサイズは直前のバッファサイズの1.5倍に拡張される。
     * この倍数を任意に指定する。
     *
     * Example :
     *     byte[] values = {1,2,3,4};
     *     ByteBuilder bb = new ByteBuilder(values);
     *     assertEquals(4, bb.length());
     *     assertEquals(4, bb.capacity());
     *
     *     bb.setBuffExpansionMultipler(2.0); <- set new value to '2.0' from '1.5' as default.
     *     bb.append((byte)0xFF);
     *     assertEquals(5, bb.length());
     *     assertEquals(8, bb.capacity()); <- auto expansion
     *
     * </PRE>
     *
     * @param value 新たなバッファサイズ拡張倍数
     */
    public void setBuffExpansionMultipler(double value){
        _BuffExpandMultipler = value;
    }



    /**
     * <PRE>
     * {@link #toString()}メソッドで16進数文字列を生成する際のフォーマットパラメータを
     * 明示的に指定する。
     * デフォルトのフォーマットパラメータは、"0x%2x"。
     *
     * Example :
     *     byte[] initValues = {0x89, 0xAB, 0xCD};
     *     ByteBuilder bb = new ByteBuilder(initValues);
     *     String s = bb.toString();
     *
     *     // s = "{0x89, 0xAB, 0xCD}"
     *
     *     bb.setFormatParam("$%04Xh");
     *     s = bb.toString();
     *
     *     // s = "{$0089h, $00ABh, $00CDh}"
     *
     * </PRE>
     *
     * @param toStringFormatter
     */
    public void setFormatParam(String toStringFormatter){
        _formatParam = toStringFormatter;
    }

    /**
     * <PRE>
     * {@link #setFormatParam(String)}で変更された、{@link #toString()}用の
     * フォーマットパラメータを、デフォルト値("0x%2x")に戻す。
     *
     * </PRE>
     *
     */
    public void resetFormatParam(){
        _formatParam = "0x%02x";
    }

    /**
     * <PRE>
     * ByteBuilderのインスタンスの中に１つ、引数で指定されたオブジェクト(Reference Object)を格納する。
     *
     * </PRE>
     *
     * @param obj
     */
    public void setReferObject(Object obj){
        _referObject = obj;
    }

    /**
     * <PRE>
     * ByteBuilderのインスタンス中に{@link #setReferObject(Object)}で格納された、
     * Reference Objectを返す。
     * </PRE>
     *
     * @return
     */
    public Object getReferObject(){
        return _referObject;
    }


}
